﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using Microsoft.Forums.SyncService;
using System.ServiceModel;
using Local = Microsoft.Forums.OfflineStore;
using Web = Microsoft.Forums.MicrosoftForumsProvider.Forums;
using Microsoft.Forums.MicrosoftForumsProvider.Forums;
using Microsoft.Forums.ForumsProvider;

namespace Microsoft.Forums.MicrosoftForumsProvider
{
    /// <summary>
    /// The synchronization service for the MSDN Forums
    /// </summary>
    public class MicrosoftForumsServiceProvider : IForumServiceProvider
    {
        public static readonly Guid ProviderGuid = new Guid("968E7B70-92A4-44ea-AFE4-7DC26A0A000C");

        // The login ticket from passport
        private string passportTicket;

        // The forum service used to conenct to the web forums
        private IFullForumsService service;


        /// <summary>
        /// Create the Microsoft Forums Service provider
        /// </summary>
        public MicrosoftForumsServiceProvider()
        {
            service = new FullForumsServiceClient();

            // This would be overridden in the application by DefaultSyncBatchSize in app.config
            BatchSize = 21;
        }

        /// <summary>
        /// Create the Microsoft Forums Service provider and pass in the ticket needed to login to the service with
        /// </summary>
        /// <remarks>If ticket is invalid (null, empty are incorrect) then you can still connect
        /// but some functions will throw unauthorized exception</remarks>
        /// <param name="ticket">login ticket from passport</param>
        public MicrosoftForumsServiceProvider(string ticket)
        {
            this.service = new FullForumsServiceClient();
            AuthenticationTicket = ticket;

            // This would be overridden in the application by DefaultSyncBatchSize in app.config
            BatchSize = 21;
        }

        /// <summary>
        /// Create the service provider given an already set up and authenticated IForumsService
        /// This is used for automated testing inorder to pass a stub service in
        /// </summary>
        /// <param name="svc">Custom forums service</param>
        public MicrosoftForumsServiceProvider(IFullForumsService svc)
        {
            service = svc;
            
            // This would be overridden in the application by DefaultSyncBatchSize in app.config
            BatchSize = 21;
        }

        /// <summary>
        /// The Uri that this service provider is connected to
        /// </summary>
        public Uri Uri
        {
            get
            {
                var forumService = service as ClientBase<IFullForumsService>;
                if (forumService != null)
                {
                    return forumService.Endpoint.Address.Uri;
                }

                return null;
            }
        }

        /// <summary>
        /// Get or set the passport ticket used to authenticate the user
        /// </summary>
        public string AuthenticationTicket
        {
            get
            {
                return passportTicket;
            }
            set
            {
                passportTicket = value;

                // Check if this is a wcf service, if so add new endpoint
                var forumService = service as ClientBase<IFullForumsService>;
                if (forumService != null)
                {
                    if (forumService.Endpoint.Behaviors.Contains(typeof(AuthenticationInspector)))
                    {
                        forumService.Endpoint.Behaviors.Remove(typeof(AuthenticationInspector));
                    }

                    forumService.Endpoint.Behaviors.Add(new AuthenticationInspector(passportTicket));

                }
            }
        }

        /// <summary>
        /// The underlying Forum Service
        /// </summary>
        public IFullForumsService ForumsService
        {
            get
            {
                return service;
            }
        }

        /// <summary>
        /// The size which we will pull batches of data from the server
        /// </summary>
        public int BatchSize
        {
            get;
            set;
        }



        #region Private Methods

        /// <summary>
        /// Repeatedly execute a function that has paging in batches and covnerts and concatenates the results.
        /// It will execute this method with new paging indicies until it returns
        /// less results than the batchSide
        /// </summary>
        /// <typeparam name="T">The type we are aggregating batches of</typeparam>
        /// <param name="generator">Function to call</param>
        /// <returns>Aggregated list of T</returns>
        private List<T> BuildResultsInBatch<T>(Func<int, int, List<T>> generator)
        {
            int index = 0;
            var concatResults = new List<T>();
            var results = new List<T>();
            do
            {
                results = generator(index, BatchSize);
                if (results != null)
                {
                    concatResults.AddRange(results);
                    index++;
                }

            } while (results != null && results.Count >= BatchSize);

            return concatResults;
        }

        #endregion

        #region IForumServiceProvider Members

        /// <summary>
        /// Pings the web service
        /// </summary>
        /// <returns> Logged in users Id if succesfull</returns>
        public string Ping()
        {
            return ForumsService.Ping();
        }

        /// <summary>
        /// The unique provider Id for this sync service
        /// </summary>
        public Guid ProviderId
        {
            get
            {
                return ProviderGuid;
            }
        }

        /// <summary>
        /// The name of this sync provider
        /// </summary>
        public string ProviderName
        {
            get
            {
                return "MSDNForums";
            }
        }

        /// <summary>
        /// Authenticates the current user for this service
        /// </summary>
        /// <returns>True if successfully authenticated</returns>
        public bool TryAuthenticate(ref AuthenticationInformation information)
        {
            try
            {
                information.UserName = PassportHelper.AuthenticateUser();
                information.Ticket = PassportHelper.AuthTicket;
            }
            catch (ApplicationException)
            {
                return false;
            }

            return true;
        }

        /// <summary>
        /// Gets a list of all the forums 
        /// </summary>
        /// <param name="brand"></param>
        /// <param name="locale"></param>
        /// <returns>List of Forums for the given brand and locale</returns>
        public IList<Local.Forum> GetAvailableForums(string brand, int locale)
        {
            if (String.IsNullOrEmpty(brand)) throw new ArgumentNullException("brand", "brand must not be null");
            if (locale <= 0) throw new ArgumentException("locale must be greater than 0", "locale");

            // This is a temporary workaround to address the last minute issue found that it only
            // only returns top 20 forums with ForumsService.GetAllForumsBrief(brand, locale);
            // Needs to be switched back after the pending bug is resolved by the API team.
            var results = ForumsService.GetAllForums(brand, locale, 0, 200);
            var res = results.Results.ConvertAll(f => DomainObjectConverters.Convert(f));

            return res;
        }

        /// <summary>
        /// Get a collection of all the supported brands
        /// </summary>
        /// <returns>collection of brands</returns>
        public IList<string> SupportedBrands
        {
            get
            {
                return ForumsService.GetActiveBrands();
            }
        }

        /// <summary>
        /// Get a collection of all the supported locales
        /// </summary>
        /// <returns>collection of locales</returns>
        public IList<KeyValuePair<int, string>> SupportedLocales
        {
            get
            {
                List<KeyValuePair<int, string>> supportedLocalesList = new List<KeyValuePair<int, string>>();
                List<string> activeLocales = ForumsService.GetActiveLocales();

                foreach (string activeLocale in activeLocales)
                {
                    try
                    {
                        supportedLocalesList.Add(new KeyValuePair<int, string>(
                         System.Globalization.CultureInfo.GetCultureInfo(activeLocale).LCID, activeLocale));
                        
                    }
                    catch(System.ArgumentException)
                    {
                        // Ignore any locale that throws System.ArgumentException because they are not supported 
                        // on this platform. Example: "en-IN" is not supported on pre-Vista operating systems
                    }
                }
                return supportedLocalesList;
            }
        }

        /// <summary>
        /// Get a collection of forum objects which have been deleted
        /// </summary>
        /// <param name="fromDate">Deletions after this date</param>
        /// <param name="toDate">Deletions before this date</param>
        /// <returns>The collection</returns>
        public IList<Local.Forum> GetDeletedForums(DateTime fromDate, DateTime toDate)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Get the forum object given a forum id
        /// </summary>
        /// <param name="forumID">The id of the forum</param>
        /// <returns>A forum object</returns>
        public Local.Forum GetForum(Guid forumId)
        {
            return DomainObjectConverters.Convert(ForumsService.GetForumBrief(forumId));
        }

        /// <summary>
        /// Gets every thread of a forum
        /// </summary>
        /// <param name="forumID">The parent forum</param>
        /// <returns>The collection of threads</returns>
        public IList<Local.Thread> GetAllThreads(Guid forumId)
        {
            return BuildResultsInBatch(
                delegate(int index, int batchSize)
                {
                    var results = ForumsService.GetForumThreadsBrief(forumId, index, batchSize, false);
                    return results.Results.ConvertAll(t => DomainObjectConverters.Convert(t));
                });
        }

        /// <summary>
        /// Get all the threads of a forum which have been update between the fromData and toDate
        /// </summary>
        /// <param name="forumID">ID of the parent forum</param>
        /// <param name="fromDate">Updated after this date</param>
        /// <param name="toDate">Updated before this date</param>
        /// <returns>Collection of updated threads</returns>
        public IList<Local.Thread> GetUpdatedThreads(Guid forumId, DateTime fromDate, DateTime toDate)
        {
            if (fromDate > toDate) throw new ArgumentOutOfRangeException("fromDate", "Invalid date range: fromDate cannot be after toDate");

            var updatedThreads = BuildResultsInBatch(
                delegate(int index, int batchSize)
                {
                    var results = ForumsService.GetUpdatedThreadsBrief(forumId, fromDate, toDate, index, batchSize, false);
                    return results.Results.ConvertAll(t => DomainObjectConverters.Convert(t));
                });

            var newThreads = BuildResultsInBatch(
                delegate(int index, int batchSize)
                {
                    var results = ForumsService.GetCreatedThreadsBrief(forumId, fromDate, toDate, index, batchSize, false);
                    return results.Results.ConvertAll(t => DomainObjectConverters.Convert(t));
                });

            // Merge the two list removing duplicates if a thread
            // was created and modified in the same time range

            foreach (var newThread in newThreads)
            {
                if (!updatedThreads.Any(t => t.Id == newThread.Id))
                {
                    updatedThreads.Add(newThread);
                }
            }

            return updatedThreads;
        }

        /// <summary>
        /// Get a collection of thread objects which have been deleted
        /// </summary>
        /// <param name="forumID">ID of the parent forum</param>
        /// <param name="fromDate">Deletions after this date</param>
        /// <param name="toDate">Deletions before this date</param>
        /// <returns>The collection</returns>
        public IList<Local.Thread> GetDeletedThreads(Guid forumId, DateTime fromDate, DateTime toDate)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Get the thread object given a forum id
        /// </summary>
        /// <param name="forumId">The forum id.</param>
        /// <param name="threadId">The thread id.</param>
        /// <returns>A thread object</returns>
        public Local.Thread GetThread(Guid forumId, Guid threadId)
        {
            return DomainObjectConverters.Convert(ForumsService.GetThreadBrief(threadId, false));
        }

        /// <summary>
        /// Gets every post of a thread
        /// </summary>
        /// <param name="forumId">The forum id.</param>
        /// <param name="threadId">The thread id.</param>
        /// <returns>The collection of posts</returns>
        public IList<Local.Post> GetAllPosts(Guid forumId, Guid threadId)
        {
            return BuildResultsInBatch(
                delegate(int index, int batchSize)
                {
                    var results = ForumsService.GetThreadMessagesBrief(threadId, index, batchSize, true);
                    return results.Results.ConvertAll(p => DomainObjectConverters.Convert(p, forumId));
                });
        }

        /// <summary>
        /// Get all the posts of a forum which have been update between the fromData and toDate
        /// </summary>
        /// <param name="forumId">The forum id.</param>
        /// <param name="threadId">The thread id.</param>
        /// <param name="fromDate">Updated after this date</param>
        /// <param name="toDate">Updated before this date</param>
        /// <returns>Collection of updated posts</returns>
        public IList<Local.Post> GetUpdatedPosts(Guid forumId, Guid threadId, DateTime fromDate, DateTime toDate)
        {
            if (fromDate > toDate) throw new ArgumentOutOfRangeException("fromDate", "Invalid date range: fromDate cannot be after toDate");

            return BuildResultsInBatch(
                delegate(int index, int batchSize)
                {
                    var results = ForumsService.GetNewOrModifiedMessagesBrief(threadId, fromDate, toDate, index, batchSize, true);
                    return results.Results.ConvertAll(p => DomainObjectConverters.Convert(p, forumId));
                });
        }

        /// <summary>
        /// Get a collection of post objects which have been deleted
        /// </summary>
        /// <param name="forumId">The forum id.</param>
        /// <param name="threadId">The thread id.</param>
        /// <param name="fromDate">Deletions after this date</param>
        /// <param name="toDate">Deletions before this date</param>
        /// <returns>The collection</returns>
        public IList<Local.Post> GetDeletedPosts(Guid forumId, Guid threadId, DateTime fromDate, DateTime toDate)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Get the post object given a forum id
        /// </summary>
        /// <param name="forumId">The forum id.</param>
        /// <param name="postId">The post id.</param>
        /// <returns>A post object</returns>
        public Local.Post GetPost(Guid forumId, Guid postId)
        {
            return DomainObjectConverters.Convert(ForumsService.GetMessageBrief(postId), forumId);
        }

        /// <summary>
        /// Gets the current user.
        /// </summary>
        /// <returns></returns>
        public DetailedUser CurrentUser
        {
            get
            {
                return DomainObjectConverters.Convert(service.GetCurrentUser());
            }
        }

        /// <summary>
        /// Creates the question thread.
        /// </summary>
        /// <param name="forumId">The forum id.</param>
        /// <param name="title">The title.</param>
        /// <param name="body">The body.</param>
        public void CreateQuestionThread(Guid forumId, string title, string body)
        {
            service.CreateQuestionThread(forumId, title, body);
        }

        /// <summary>
        /// Creates the comment thread.
        /// </summary>
        /// <param name="forumId">The forum id.</param>
        /// <param name="title">The title.</param>
        /// <param name="body">The body.</param>
        public void CreateCommentThread(Guid forumId, string title, string body)
        {
            service.CreateCommentThread(forumId, title, body);
        }

        /// <summary>
        /// Creates the reply.
        /// </summary>
        /// <param name="threadId">The thread id.</param>
        /// <param name="postId">The post id.</param>
        /// <param name="body">The body.</param>
        public void CreateReply(Guid threadId, Guid postId, string body)
        {
            service.CreateReply(threadId, postId, body);
        }

        /// <summary>
        /// Marks the post as answer.
        /// </summary>
        /// <param name="postId">The post id.</param>
        public void MarkPostAsAnswer(Guid postId)
        {
            service.MarkPostAsAnswer(postId);
        }

        /// <summary>
        /// Unmark the post as answer.
        /// </summary>
        /// <param name="postId">The post id.</param>
        public void UnmarkPostAsAnswer(Guid postId)
        {
            service.UnMarkPostAsAnswer(postId);
        }

        #endregion
    }
}
